package edu.udo.sopra10.chaturaji.controllers;

import static edu.udo.sopra10.chaturaji.controllers.GameController.controlEventSuppression;
import static edu.udo.sopra10.chaturaji.controllers.GameController.enterRoll;
import static edu.udo.sopra10.chaturaji.controllers.GameController.getGame;
import static edu.udo.sopra10.chaturaji.controllers.GameController.initBoard;
import static edu.udo.sopra10.chaturaji.controllers.GameController.move;
import static edu.udo.sopra10.chaturaji.controllers.GameController.pass;
import static edu.udo.sopra10.chaturaji.controllers.GameController.setGame;
import static edu.udo.sopra10.chaturaji.controllers.GameController.setPreliminaryGame;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNull;
import static java.lang.Byte.parseByte;
import static java.lang.Integer.MIN_VALUE;
import static java.lang.Integer.parseInt;
import static java.lang.Math.min;
import static java.lang.Math.max;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.nio.charset.Charset.forName;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import edu.udo.sopra10.chaturaji.entities.Game;
import edu.udo.sopra10.chaturaji.entities.Move;
import edu.udo.sopra10.chaturaji.entities.PieceMove;
import edu.udo.sopra10.chaturaji.entities.Player;
import edu.udo.sopra10.chaturaji.entities.Square;
import edu.udo.sopra10.chaturaji.entities.Turn;
import edu.udo.sopra10.chaturaji.entities.Player.Type;
import edu.udo.sopra10.chaturaji.entities.coordinates.Position;
import edu.udo.sopra10.chaturaji.entities.pieces.Boat;
import edu.udo.sopra10.chaturaji.entities.pieces.Elephant;
import edu.udo.sopra10.chaturaji.entities.pieces.Horse;
import edu.udo.sopra10.chaturaji.entities.pieces.King;
import edu.udo.sopra10.chaturaji.entities.pieces.Pawn;
import edu.udo.sopra10.chaturaji.entities.pieces.Piece;
import edu.udo.sopra10.chaturaji.entities.terminationconditions.Elimination;
import edu.udo.sopra10.chaturaji.entities.terminationconditions.RoundLimit;
import edu.udo.sopra10.chaturaji.entities.terminationconditions.ScoreLimit;
import edu.udo.sopra10.chaturaji.entities.terminationconditions.TerminationCondition;
import edu.udo.sopra10.chaturaji.entities.terminationconditions.TotalElimination;
import edu.udo.sopra10.chaturaji.entities.transfer.TransferTerminationCondition;

/**
 * The controller singleton for everything that is not a game. This implements
 * all meta-game features such as saving and loading as well as access to the
 * current configuration.
 * 
 * @author Simon Dierl
 */
public final class SystemController {
	/**
	 * The configuration and highscore.
	 */
	private static final Properties config;

	/**
	 * The path to the {@code .chaturaji.xml}, the game configuration.
	 */
	private final static String CONFIG_PATH;

	/**
	 * The path to the default {@code .chaturaji.xml} shipped with the software.
	 */
	private final static String DEFAULT_CONFIG_PATH = "/chaturaji-default.xml";

	/**
	 * The savegame format version string.
	 */
	private static final String SAVEGAME_FORMAT_VERSION = "chaturaji-savegame-format=1";

	/**
	 * The UTF-8 character set.
	 */
	private static final Charset UTF_8 = forName("UTF-8");

	static {
		String appData = getenv("APPDATA");
		if (appData == null) {
			appData = getProperty("user.home");
		}
		CONFIG_PATH = new StringBuilder(appData).append(File.separatorChar)
				.append(".chaturaji.xml").toString();

		config = new Properties();
		try {
			final InputStream configStream = new FileInputStream(CONFIG_PATH);
			config.loadFromXML(configStream);
			configStream.close();
		} catch (final FileNotFoundException e) {
			final InputStream defaultConfigStream = SystemController.class
					.getResourceAsStream(DEFAULT_CONFIG_PATH);
			try {
				config.loadFromXML(defaultConfigStream);
				defaultConfigStream.close();
				storeConfiguration();
			} catch (final IOException e1) {
				throw new IllegalStateException(e);
			}
		} catch (final IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Get all highscore holders' names.
	 * 
	 * @param terminationCondition
	 *            the highscore category, must not be {@code null}.
	 * @return the highscore holder's names, never {@code null}. never contains
	 *         {@code null}.
	 */
	public static List<String> getHighscoreNames(
			final TerminationCondition terminationCondition) {
		String baseName;
		if (terminationCondition instanceof Elimination) {
			baseName = "highscore.name.elimination.";
		} else if (terminationCondition instanceof RoundLimit) {
			baseName = "highscore.name.round-limit.";
		} else if (terminationCondition instanceof ScoreLimit) {
			baseName = "highscore.name.score-limit.";
		} else if (terminationCondition instanceof TotalElimination) {
			baseName = "highscore.name.total-elimination.";
		} else {
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition.toString());
		}
		List<String> names = new LinkedList<String>();
		for (int i = 0;; i++) {
			String name = config.getProperty(baseName + i);
			if (name == null) {
				break;
			} else {
				names.add(name);
			}
		}
		return names;
	}

	/**
	 * Get all highscore.
	 * 
	 * @param terminationCondition
	 *            the highscore category, must not be {@code null}.
	 * @return the highscores, never {@code null}. never contains {@code null}.
	 */
	public static List<Integer> getHighscoreScores(
			final TerminationCondition terminationCondition) {
		String baseName;
		if (terminationCondition instanceof Elimination) {
			baseName = "highscore.score.elimination.";
		} else if (terminationCondition instanceof RoundLimit) {
			baseName = "highscore.score.round-limit.";
		} else if (terminationCondition instanceof ScoreLimit) {
			baseName = "highscore.score.score-limit.";
		} else if (terminationCondition instanceof TotalElimination) {
			baseName = "highscore.score.total-elimination.";
		} else {
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition.toString());
		}
		List<Integer> scores = new LinkedList<Integer>();
		for (int i = 0;; i++) {
			String score = config.getProperty(baseName + i);
			if (score == null) {
				break;
			} else {
				scores.add(parseInt(score));
			}
		}
		return scores;
	}

	/**
	 * Get the password for the mail server.
	 * 
	 * @return the mail password, never {@code null}.
	 */
	public static String getMailPassword() {
		return config.getProperty("mail.password");
	}

	/**
	 * Get the mail server hostname.
	 * 
	 * @return the mail server hostname, never {@code null}.
	 */
	public static String getMailServer() {
		return config.getProperty("mail.server");
	}

	/**
	 * Get the username for the mail server.
	 * 
	 * @return the mail username, never {@code null}.
	 */
	public static String getMailUsername() {
		return config.getProperty("mail.username");
	}

	/**
	 * Get the mail address used for sending.
	 * 
	 * @return the mail address, never {@code null}.
	 */
	public static String getSenderMailAddress() {
		return config.getProperty("mail.sender");
	}

	/**
	 * Load a game from the specified file.
	 * 
	 * @param savegame
	 *            the game to load.
	 * @throws IOException
	 *             if reading fails.
	 */
	public static void loadGame(final File savegame) throws IOException {
		final BufferedReader bufferedReader = new BufferedReader(
				new InputStreamReader(new FileInputStream(savegame), UTF_8));
		final Game game = deserializeGameHeader(bufferedReader);
		setPreliminaryGame(game);
		initBoard();
		controlEventSuppression(true);
		deserializeTurnsRecursive(game, (byte) 0, bufferedReader);
		controlEventSuppression(false);
		bufferedReader.close();
		for (final Player player : game.getPlayers()) {
			player.taint();
		}
		setGame(game);
	}

	/**
	 * Create a new game.
	 * 
	 * @param name1
	 *            the first player's name.
	 * @param type1
	 *            the first player's type.
	 * @param name2
	 *            the second player's name.
	 * @param type2
	 *            the second player's type.
	 * @param name3
	 *            the third player's name.
	 * @param type3
	 *            the third player's type.
	 * @param name4
	 *            the fourth player's name.
	 * @param type4
	 *            the fourth player's type.
	 * @param terminationCondition
	 *            the id of the termination condition.
	 * @param terminationConditionParams
	 *            the constructor parameters for the termination condition.
	 */
	public static void newGame(final String name1, final Type type1,
			final String name2, final Type type2, final String name3,
			final Type type3, final String name4, final Type type4,
			final TransferTerminationCondition terminationCondition,
			final Object... terminationConditionParams) {
		final TerminationCondition condition;
		switch (assertNotNull(terminationCondition, "termination condition")) {
		case ELIMINATION:
			if (terminationConditionParams.length != 0) {
				throw new IllegalArgumentException("mode accepts no params");
			}
			condition = new Elimination(null);
			break;
		case ROUND_LIMIT:
			if (terminationConditionParams.length != 1) {
				throw new IllegalArgumentException("mode accepts one param");
			}
			condition = new RoundLimit(null,
					(Integer) terminationConditionParams[0]);
			break;
		case SCORE_LIMIT:
			if (terminationConditionParams.length != 1) {
				throw new IllegalArgumentException("mode accepts one param");
			}
			condition = new ScoreLimit(null,
					(Integer) terminationConditionParams[0]);
			break;
		case TOTAL_ELIMINATION:
			if (terminationConditionParams.length != 0) {
				throw new IllegalArgumentException("mode accepts no params");
			}
			condition = new TotalElimination(null);
			break;
		default:
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition);
		}
		final Game game = new Game(condition);
		condition.setGame(game);

		game.addPlayer(new Player(game, name1, type1, (byte) 0));
		game.addPlayer(new Player(game, name2, type2, (byte) 1));
		game.addPlayer(new Player(game, name3, type3, (byte) 2));
		game.addPlayer(new Player(game, name4, type4, (byte) 3));

		setGame(game);
	}

	/**
	 * Take note of a highscore, if applicable.
	 * 
	 * @param game
	 *            the game for which highscores should be recorded.
	 */
	public static void recordHighscore(final Game game) {
		final TerminationCondition terminationCondition = game
				.getTerminationCondition();
		List<String> bestNames = getHighscoreNames(terminationCondition);
		List<Integer> bestScores = getHighscoreScores(terminationCondition);
		String bestName = null;
		int bestScore = MIN_VALUE;
		for (final Player player : game.getPlayers()) {
			if (!player.isTainted() && (player.getScore() > bestScore)) {
				bestName = player.getName();
				bestScore = player.getScore();
			}
		}

		int rank = 0;
		for (int score : bestScores) {
			if (bestScore > score) {
				break;
			}
			rank++;
		}
		if (rank > 9) {
			return;
		}

		List<String> newBestNames = new LinkedList<String>();
		List<Integer> newBestScores = new LinkedList<Integer>();

		if (!bestNames.isEmpty()) {
			newBestNames.addAll(bestNames.subList(0, rank));
		}
		newBestNames.add(bestName);
		if (bestNames.size() >= rank) {
			newBestNames.addAll(bestNames.subList(rank,
					max(min(bestNames.size() - 1, 9), 0)));
		}
		if (!bestScores.isEmpty()) {
			newBestScores.addAll(bestScores.subList(0, rank));
		}
		newBestScores.add(bestScore);
		if (bestScores.size() >= rank) {
			newBestScores.addAll(bestScores.subList(rank,
					max(min(bestScores.size() - 1, 9), 0)));
		}

		setHighscoreNames(terminationCondition, bestNames);
		setHighscoreScores(terminationCondition, bestScores);
	}

	/**
	 * Save the current game.
	 * 
	 * @param savegame
	 *            the path of the savegame.
	 * @throws IOException
	 *             if writing fails.
	 */
	public static void saveGame(final File savegame) throws IOException {
		for (final Player player : getGame().getPlayers()) {
			player.taint();
		}
		savegame.createNewFile();
		final Writer writer = new OutputStreamWriter(new FileOutputStream(
				savegame), UTF_8);
		writer.write(serializeGame(getGame()));
		writer.close();
	}

	/**
	 * Alter the mail server password.
	 * 
	 * @param mailPassword
	 *            the new password, must not be @code null.
	 */
	public static void setMailPassword(final String mailPassword) {
		config.setProperty("mail.password", mailPassword);
		storeConfiguration();
	}

	/**
	 * Alter the mail server hostname.
	 * 
	 * @param mailServer
	 *            the new hostname, must not be @code null.
	 */
	public static void setMailServer(final String mailServer) {
		config.setProperty("mail.server", mailServer);
		storeConfiguration();
	}

	/**
	 * Alter the mail server username.
	 * 
	 * @param mailUsername
	 *            the new username, must not be @code null.
	 */
	public static void setMailUsername(final String mailUsername) {
		config.setProperty("mail.username", mailUsername);
		storeConfiguration();
	}

	/**
	 * Alter the mail address used for sending mails..
	 * 
	 * @param senderMailAddress
	 *            the new mail address, must not be @code null.
	 */
	public static void setSenderMailAddress(final String senderMailAddress) {
		config.setProperty("mail.sender", senderMailAddress);
		storeConfiguration();
	}

	/**
	 * Read the termination condition from a savegame.
	 * 
	 * @param reader
	 *            the savegame reader, must not be {@code null}.
	 * @return the game with the read termination condition, never {@code null}.
	 * @throws IOException
	 *             if reading fails.
	 */
	private static Game deserializeMode(final BufferedReader reader)
			throws IOException {
		final StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
		final String mode = tokenizer.nextToken();
		TerminationCondition terminationCondition;
		if (mode.equals("elimination")) {
			terminationCondition = new Elimination(null);
		} else if (mode.equals("round-limit")) {
			terminationCondition = new RoundLimit(null,
					parseInt(tokenizer.nextToken()));
		} else if (mode.equals("score-limit")) {
			terminationCondition = new ScoreLimit(null,
					parseInt(tokenizer.nextToken()));
		} else if (mode.equals("total-elimination")) {
			terminationCondition = new TotalElimination(null);
		} else {
			throw new IllegalStateException("unknown mode " + mode);
		}
		final Game game = new Game(terminationCondition);
		terminationCondition.setGame(game);
		return game;
	}

	/**
	 * Read a single move from a savegame and make it on the current game.
	 * 
	 * @param game
	 *            the game to write to, must not be {@code null}.
	 * @param line
	 *            the line containing the move, must not be {@code null}.
	 * @return if the move was a pass.
	 */
	private static boolean deserializeMove(final Game game, final String line) {
		final StringTokenizer tokenizer = new StringTokenizer(line);
		tokenizer.nextToken();
		tokenizer.nextToken();
		final String moveClass = tokenizer.nextToken();
		final Square[][] board = getGame().getBoard();

		Class<? extends Piece> expectedPiece;
		if (moveClass.equals("B")) {
			expectedPiece = Pawn.class;
		} else if (moveClass.equals("S")) {
			expectedPiece = Boat.class;
		} else if (moveClass.equals("P")) {
			expectedPiece = Horse.class;
		} else if (moveClass.equals("E")) {
			expectedPiece = Elephant.class;
		} else if (moveClass.equals("K")) {
			expectedPiece = King.class;
		} else if (moveClass.equals("pass")) {
			return true;
		} else if (moveClass.equals("unused")) {
			return false;
		} else {
			throw new IllegalStateException("unknown move class " + moveClass);
		}

		final Position start = deserializePosition(tokenizer.nextToken(),
				tokenizer.nextToken());
		final Position end = deserializePosition(tokenizer.nextToken(),
				tokenizer.nextToken());
		final Piece piece = board[start.getX()][start.getY()].getPiece();
		if (!expectedPiece.isInstance(piece)) {
			throw new IllegalStateException("bad piece type given: "
					+ piece.getClass() + ", expected " + expectedPiece);
		}

		move(start, end);
		return false;
	}

	/**
	 * Read a player form a savegame.
	 * 
	 * @param game
	 *            the game to write to, must not be {@code null}.
	 * @param expectedColor
	 *            the expected color string for the player, must not be
	 *            {@code null}.
	 * @param position
	 *            the players position, must not be in {@code [0, 3]}.
	 * @param reader
	 *            the savegame reader, must not be {@code null}.
	 * @return the altered game.
	 * @throws IOException
	 *             if reading fails.
	 */
	private static Game deserializePlayer(final Game game,
			final String expectedColor, final byte position,
			final BufferedReader reader) throws IOException {
		final StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
		final String color = tokenizer.nextToken();
		if (!color.equals(expectedColor)) {
			throw new IllegalStateException("wrong color " + color
					+ ", expected " + expectedColor);
		}
		final String name = tokenizer.nextToken();
		final String type = tokenizer.nextToken();
		game.addPlayer(new Player(game, name, Type.valueOf(type), position));
		return game;
	}

	/**
	 * Transform a board position into a {@link Position}.
	 * 
	 * @param x
	 *            the x coordinate string, must not be {@code null}.
	 * @param y
	 *            the y coordinate string, must not be {@code null}.
	 * @return the read position.
	 */
	private static Position deserializePosition(final String x, final String y) {
		return new Position((byte) (x.charAt(0) - 'a'),
				(byte) (8 - parseByte(y)));
	}

	/**
	 * Read all turns from a save game and append them to the current game.
	 * 
	 * @param game
	 *            the game to write to, must not be {@code null}.
	 * @param expectedPosition
	 *            the expected player position, must not be in {@code [0, 3]}.
	 * @param reader
	 *            the save game reader, must not be {@code null}.
	 * @return the altered game, never {@code null}.
	 * @throws IOException
	 *             if reading fails.
	 */
	private static Game deserializeTurnsRecursive(final Game game,
			final Byte expectedPosition, final BufferedReader reader)
			throws IOException {
		final String line1 = reader.readLine();
		if (line1 == null) {
			return game;
		}
		final String line2 = reader.readLine();

		String expectedColor = null;
		switch (expectedPosition) {
		case 0:
			expectedColor = "ge";
			break;
		case 1:
			expectedColor = "sw";
			break;
		case 2:
			expectedColor = "ro";
			break;
		case 3:
			expectedColor = "gr";
			break;
		}
		StringTokenizer tokenizer = new StringTokenizer(line1);
		final byte roll1 = parseByte(tokenizer.nextToken());
		final String color1 = tokenizer.nextToken();
		tokenizer = new StringTokenizer(line2);
		final byte roll2 = parseByte(tokenizer.nextToken());
		final String color2 = tokenizer.nextToken();
		if (!color1.equals(expectedColor)) {
			throw new IllegalStateException("wrong color " + color1
					+ ", expected " + expectedColor);
		}
		if (!color2.equals(expectedColor)) {
			throw new IllegalStateException("wrong color " + color2
					+ ", expected " + expectedColor);
		}

		enterRoll(roll1, roll2);

		final boolean passed = deserializeMove(game, line1)
				| deserializeMove(game, line2);

		if (passed) {
			pass();
		}

		return deserializeTurnsRecursive(game,
				(byte) ((expectedPosition + 1) % 4), reader);
	}

	/**
	 * Serialize a game to a {@link StringBuilder}.
	 * 
	 * @param game
	 *            the game to serialize, must not be {@code null}.
	 * @param stringBuilder
	 *            the string builder to write to, must not be {@code null}.
	 * @return the {@code stringBuilder}, never {@code null}.
	 * @see #serializeGame(Game)
	 */
	private static StringBuilder serializeGame(final Game game,
			final StringBuilder stringBuilder) {
		serializePlayers(game, serializeMode(game, stringBuilder).append("\n"))
				.append("\n");
		for (final Turn turn : game.getHistory()) {
			serializeTurn(turn, stringBuilder).append("\n");
		}
		return stringBuilder;
	}

	/**
	 * Serialize a game's mode to a {@link StringBuilder}.
	 * 
	 * @param game
	 *            the game to serialize, must not be {@code null}.
	 * @param stringBuilder
	 *            the string builder to write to, must not be {@code null}.
	 * @return the {@code stringBuilder}, never {@code null}.
	 * @see #serializeGame(Game)
	 */
	private static StringBuilder serializeMode(final Game game,
			final StringBuilder stringBuilder) {
		final TerminationCondition terminationCondition = game
				.getTerminationCondition();
		if (terminationCondition instanceof Elimination) {
			return stringBuilder.append("elimination");
		} else if (terminationCondition instanceof RoundLimit) {
			return stringBuilder.append("round-limit ").append(
					((RoundLimit) terminationCondition).getLimit());
		} else if (terminationCondition instanceof ScoreLimit) {
			return stringBuilder.append("score-limit ").append(
					((ScoreLimit) terminationCondition).getLimit());
		} else if (terminationCondition instanceof TotalElimination) {
			return stringBuilder.append("total-elimination");
		} else {
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition.toString());
		}
	}

	/**
	 * Serialize a move to a {@link StringBuilder}.
	 * 
	 * @param move
	 *            the move to serialize, must not be {@code null}.
	 * @param unassignedRolls
	 *            an arbitrarily sorted list of the turn's unassigned rolls,
	 *            must neither be nor contain {@code null} and be in
	 *            {@code [1, 6]}.
	 * @param playerColor
	 *            the string representation of the player's color, surrounded by
	 *            whitespace, must not be {@code null}.
	 * @param stringBuilder
	 *            the string builder to write to, must not be {@code null}.
	 * @return the {@code stringBuilder}, never {@code null}.
	 * @see #serializeTurn(Turn)
	 */
	private static StringBuilder serializeMove(final Move move,
			final List<Byte> unassignedRolls, final String playerColor,
			final StringBuilder stringBuilder) {
		if (move == null) {
			stringBuilder.append(unassignedRolls.get(0)).append(playerColor)
					.append("unused");
			unassignedRolls.remove(0);
			return stringBuilder;
		} else if (move instanceof PieceMove) {
			final PieceMove pieceMove = (PieceMove) move;
			stringBuilder.append(pieceMove.getDiceRoll()).append(playerColor);
			final Piece piece = pieceMove.getMovedPiece();
			if (piece instanceof Pawn) {
				stringBuilder.append("B ");
			} else if (piece instanceof Boat) {
				stringBuilder.append("S ");
			} else if (piece instanceof Horse) {
				stringBuilder.append("P ");
			} else if (piece instanceof Elephant) {
				stringBuilder.append("E ");
			} else if (piece instanceof King) {
				stringBuilder.append("K ");
			} else {
				throw new IllegalArgumentException("unknown piece type of "
						+ piece.toString());
			}
			final Position start = pieceMove.getStartPosition();
			final Position end = pieceMove.getEndPosition();
			return stringBuilder.append((char) ('a' + start.getX()))
					.append(" ").append(8 - start.getY()).append(" ")
					.append((char) ('a' + end.getX())).append(" ")
					.append(8 - end.getY());
		} else {
			return stringBuilder.append(move.getDiceRoll()).append(playerColor)
					.append("pass");
		}
	}

	/**
	 * Serialize a game's players to a {@link StringBuilder}.
	 * 
	 * @param game
	 *            the game to serialize, must not be {@code null}.
	 * @param stringBuilder
	 *            the string builder to write to, must not be {@code null}.
	 * @return the {@code stringBuilder}, never {@code null}.
	 * @see #serializeGame(Game)
	 */
	private static StringBuilder serializePlayers(final Game game,
			final StringBuilder stringBuilder) {
		final List<Player> players = game.getPlayers();
		final Player yellow = players.get(0);
		final Player black = players.get(1);
		final Player red = players.get(2);
		final Player green = players.get(3);
		return stringBuilder.append("ge ").append(yellow.getName()).append(" ")
				.append(yellow.getType()).append("\nsw ")
				.append(black.getName()).append(" ").append(black.getType())
				.append("\nro ").append(red.getName()).append(" ")
				.append(red.getType()).append("\ngr ").append(green.getName())
				.append(" ").append(green.getType());
	}

	/**
	 * Serialize a turn to a {@link StringBuilder}.
	 * 
	 * @param turn
	 *            the turn to serialize, must not be {@code null}.
	 * @param stringBuilder
	 *            the string builder to write to, must not be {@code null}.
	 * @return the {@code stringBuilder}, never {@code null}.
	 * @see #serializeTurn(Turn)
	 */
	private static StringBuilder serializeTurn(final Turn turn,
			final StringBuilder stringBuilder) {
		final List<Byte> unassignedRolls = new ArrayList<Byte>(
				turn.getUnassignedRolls());
		if (unassignedRolls.isEmpty() && (turn.getFirstMove() == null)) {
			return stringBuilder; // not yet rolled, write nothing
		}
		final String playerColor;
		switch (turn.getPlayer().getTurnOrderPosition()) {
		case 0:
			playerColor = " ge ";
			break;
		case 1:
			playerColor = " sw ";
			break;
		case 2:
			playerColor = " ro ";
			break;
		case 3:
			playerColor = " gr ";
			break;
		default:
			throw new IllegalStateException("illlegal turn order position");
		}
		return serializeMove(
				turn.getSecondMove(),
				unassignedRolls,
				playerColor,
				serializeMove(turn.getFirstMove(), unassignedRolls,
						playerColor, stringBuilder).append("\n"));
	}

	/**
	 * Alter the highscore holders.
	 * 
	 * @param terminationCondition
	 *            the highscore category to alter, must not be {@code null}.
	 * @param names
	 *            the new highscore holders, must not be {@code null} or contain
	 *            {@code null}.
	 */
	private static void setHighscoreNames(
			final TerminationCondition terminationCondition,
			final List<String> names) {
		String baseName;
		if (terminationCondition instanceof Elimination) {
			baseName = "highname.name.elimination.";
		} else if (terminationCondition instanceof RoundLimit) {
			baseName = "highname.name.round-limit.";
		} else if (terminationCondition instanceof ScoreLimit) {
			baseName = "highname.name.name-limit.";
		} else if (terminationCondition instanceof TotalElimination) {
			baseName = "highname.name.total-elimination.";
		} else {
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition.toString());
		}
		int i = 0;
		for (String name : names) {
			config.setProperty(baseName + i++, name);
		}
		storeConfiguration();
	}

	/**
	 * Alter all highscores.
	 * 
	 * @param terminationCondition
	 *            the highscore category to alter, must not be {@code null}.
	 * @param scores
	 *            the new scores, must not be {@code null} or contain
	 *            {@code null}.
	 */
	private static void setHighscoreScores(
			final TerminationCondition terminationCondition,
			final List<Integer> scores) {
		String baseName;
		if (terminationCondition instanceof Elimination) {
			baseName = "highscore.score.elimination.";
		} else if (terminationCondition instanceof RoundLimit) {
			baseName = "highscore.score.round-limit.";
		} else if (terminationCondition instanceof ScoreLimit) {
			baseName = "highscore.score.score-limit.";
		} else if (terminationCondition instanceof TotalElimination) {
			baseName = "highscore.score.total-elimination.";
		} else {
			throw new IllegalStateException("unknown termination condition "
					+ terminationCondition.toString());
		}
		int i = 0;
		for (int score : scores) {
			config.setProperty(baseName + i++, Integer.toString(score));
		}
		storeConfiguration();
	}

	/**
	 * Write the configuration and highscore to disk. Should be called after
	 * changing the {@link #config}.
	 */
	private static void storeConfiguration() {
		try {
			final OutputStream configStream = new FileOutputStream(CONFIG_PATH);
			config.storeToXML(configStream,
					"Chaturaji configuration/highscore file, edit with care");
			configStream.close();
		} catch (final IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Load a game header (metadata about players) from a savegame.
	 * 
	 * @param reader
	 *            the savegame reader, must not be {@code null}.
	 * @return the game with filled metadata, never {@code null}.
	 * @throws IOException
	 *             if reading failed.
	 */
	private static Game deserializeGameHeader(final BufferedReader reader)
			throws IOException {
		if (!reader.readLine().equals(SAVEGAME_FORMAT_VERSION)) {
			throw new IllegalStateException("invalid savegame format");
		}
		final Game game = deserializePlayer(
				deserializePlayer(
						deserializePlayer(
								deserializePlayer(deserializeMode(reader),
										"ge", (byte) 0, reader), "sw",
								(byte) 1, reader), "ro", (byte) 2, reader),
				"gr", (byte) 3, reader);
		return game;
	}

	/**
	 * Create a full serialization of a game. This format is proprietary and for
	 * use in savegames only.
	 * 
	 * @param game
	 *            the game to serialize, must not be {@code null}.
	 * @return the serialized game, never {@code null}.
	 */
	static String serializeGame(final Game game) {
		return serializeGame(game,
				new StringBuilder(SAVEGAME_FORMAT_VERSION).append("\n"))
				.toString();
	}

	/**
	 * Create the serialization of a single move. When passed {@code null}, this
	 * method will create a proprietary format. Otherwise, a move is serialized
	 * to the official SoPra format described at
	 * {@link "https://sopra.cs.uni-dortmund.de/wiki/sopra10b/projekt2"}. This
	 * format is suitable for use in e-mail games.
	 * 
	 * @param turn
	 *            the turn in which the move is contained, must not be
	 *            {@code null}.
	 * @param move
	 *            the move to serialize, must not be {@code null}.
	 * @return the serialized move, never {@code null}.
	 */
	static String serializeMove(final Turn turn, final Move move) {
		switch (turn.getPlayer().getTurnOrderPosition()) {
		case 0:
			return serializeMove(move, null, " ge ", new StringBuilder())
					.toString();
		case 1:
			return serializeMove(move, null, " sw ", new StringBuilder())
					.toString();
		case 2:
			return serializeMove(move, null, " ro ", new StringBuilder())
					.toString();
		case 3:
			return serializeMove(move, null, " gr ", new StringBuilder())
					.toString();
		default:
			throw new IllegalStateException("illlegal turn order position");
		}
	}

	/**
	 * Create the serialization of a single turn. When passed an incomplete
	 * turn, this method may create a proprietary format. A complete turn is
	 * serialized to the official SoPra format described at
	 * {@link "https://sopra.cs.uni-dortmund.de/wiki/sopra10b/projekt2"}. This
	 * format is suitable for use in e-mail games.
	 * 
	 * @param turn
	 *            the turn to serialize, must not be {@code null}.
	 * @return the serialized turn, never {@code null}.
	 */
	static String serializeTurn(final Turn turn) {
		return serializeTurn(turn, new StringBuilder()).toString();
	}

	/**
	 * Always throw a {@link UnsupportedOperationException} to prevent
	 * instantiation.
	 */
	private SystemController() {
		throw new UnsupportedOperationException("can't instantiate singleton");
	}
	
	
	/**
	 * Spawns a new thread and runs the runnable.
	 * 
	 * @param runnable the runnable to run.
	 */
	public static void spawnThread ( Runnable runnable )
	{
		new Thread( runnable ).start();
	}
}
